package com.rojored.layouts
{

import Box2DAS.Common.V2;
import Box2DAS.Common.b2Def;
import Box2DAS.Dynamics.b2Body;
import Box2DAS.Dynamics.b2World;
import com.rojored.layouts.box2DLayoutClasses.BoxPhysicsCorrelate;
import com.rojored.layouts.box2DLayoutClasses.IPhyicsCorrelate;
import flash.events.Event;
import flash.events.TimerEvent;
import flash.geom.Matrix;
import flash.geom.Matrix3D;
import flash.geom.Vector3D;
import flash.geom.Point;
import flash.utils.Timer;
import mx.core.ClassFactory;
import mx.core.IFactory;
import mx.core.ILayoutElement;
import spark.components.supportClasses.GroupBase;
import spark.layouts.supportClasses.LayoutBase;

/**
 *  Controls
 *
 *  TODO: cleanup API, what should be in meters, what in pixels?
 *  TODO: should positions be described in Vector3D?
 *  TODO: what should be in degrees, what in radians.
 */
public class Box2DLayout extends LayoutBase
{

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor
     */
    public function Box2DLayout()
    {
        super();
        itemPhysicsCorrelate = new ClassFactory(BoxPhysicsCorrelate);
    }

    //--------------------------------------------------------------------------
    //
    //  Variables
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private var boundingBox:BoundingBox;

    /**
     *  @private
     */
    private var timer:Timer;

    /**
     *  @private
     */
    private var correlates:Vector.<IPhyicsCorrelate>;

    //--------------------------------------------------------------------------
    //
    //  Overriden properties: LayoutBase
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override public function set target(value:GroupBase):void
    {
        if (super.target == value)
            return;

        stop();
        super.target = value;
    }

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------
    
    //--------------------------------------
    //  debugStepFraction 
    //--------------------------------------
    
    /**
     *  Fraction of step to apply.  Effectively slows downs or speeds up the
     *  simulation.
     */
    public var debugStepFraction:Number = 1;

    //--------------------------------------
    //  world
    //--------------------------------------

    /**
     *  @private
     *  Storage for the world property
     */
    private var _world:b2World;

    [Bindable("worldChanged")]

    /**
     *  Box2D world.
     */
    public function get world():b2World
    {
        return _world;
    }

    /**
     *  @private
     */
    public function set world(value:b2World):void
    {
        if (_world == value)
            return;

        _world = value;
        dispatchEvent(new Event("worldChanged"));
    }

    //--------------------------------------
    //  itemPhysicsCorrelate
    //--------------------------------------

    /**
     *  @private
     *  Storage for the itemPhysicsCorrelate property
     */
    private var _itemPhysicsCorrelator:IFactory;

    /**
     *  @private
     */
    private var itemPhysicsCorrelatorChanged:Boolean;

    /**
     *  Custom item correlator for the control.
     */
    public function get itemPhysicsCorrelate():IFactory
    {
        return _itemPhysicsCorrelator;
    }

    /**
     *  @private
     */
    public function set itemPhysicsCorrelate(value:IFactory):void
    {
        if (_itemPhysicsCorrelator == value)
            return;

        _itemPhysicsCorrelator = value;
        itemPhysicsCorrelatorChanged = true;

        if (target)
            target.invalidateDisplayList();
    }

    //--------------------------------------
    //  pixelsToMeters
    //--------------------------------------

    /**
     *  @private
     *  Storage for the pixelsToMeters property
     */
    private var _pixelsToMeters:Number = 30;

    /**
     *  Box2D ratio of pixels to meters.
     */
    public function get pixelsToMeters():Number
    {
        return _pixelsToMeters;
    }

    /**
     *  @private
     */
    public function set pixelsToMeters(value:Number):void
    {
        if (_pixelsToMeters == value)
            return;

        _pixelsToMeters = value;
    }

    //--------------------------------------
    //  stepFrequency
    //--------------------------------------

    /**
     *  @private
     *  Storage for the stepFrequency property
     */
    private var _stepFrequency:Number = 30;

    /**
     *  Frequency in Hz at which the physics world should be stepped.
     */
    public function get stepFrequency():Number
    {
        return _stepFrequency;
    }

    /**
     *  @private
     */
    public function set stepFrequency(value:Number):void
    {
        if (_stepFrequency == value)
            return;

        _stepFrequency = value;

        if (!isStepping)
            return;

        stop();
        timer.delay = 1000 / stepFrequency;
        start();
    }

    //--------------------------------------
    // isStepping
    //--------------------------------------

    /**
     *  @private
     *  Storage for the isStepping property
     */
    private var _isStepping:Boolean;

    [Bindable("isSteppingChanged")]

    /**
     *  If <code>true</code> the physic simulation is currently being run.
     */
    public function get isStepping():Boolean
    {
        return _isStepping;
    }

    /**
     *  @private
     */
    public function setIsStepping(value:Boolean):void
    {
        if (_isStepping == value)
            return;

        _isStepping = value;
        dispatchEvent(new Event("isSteppingChanged"));
    }

    //--------------------------------------------------------------------------
    //
    //  Overriden methods: LayoutBase
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    override public function measure():void
    {
        target.measuredWidth = 600;
        target.measuredHeight = 600;
    }

    /**
     *  @private
     */
    override public function updateDisplayList(width:Number,
                                               height:Number):void
    {
        if (!world)
            initializeDefaultWorld();

        validateWorldBoundaries(width, height);

        if (itemPhysicsCorrelatorChanged)
        {
            itemPhysicsCorrelatorChanged = false;
            destroyCorrelates();
        }

        if (!correlates)
            correlates = new Vector.<IPhyicsCorrelate>();

        var element:ILayoutElement;
        var correlate:IPhyicsCorrelate;
        var n:int = target.numElements;
        var centerX:Number;
        var centerY:Number;
        var angle:Number;
        var widthPre:Number;
        var heightPre:Number;
        var matrix:Matrix3D;
        for (var i:int = 0; i < n; i++)
        {
            element = useVirtualLayout ?  target.getVirtualElementAt(i) :
                target.getElementAt(i);

            element.setLayoutBoundsSize(NaN, NaN);

            widthPre = element.getPreferredBoundsWidth(false);
            heightPre = element.getPreferredBoundsHeight(false);
            matrix = element.getLayoutMatrix3D();
            matrix.prependTranslation(widthPre / 2, heightPre / 2, 0);
            angle = matrix.decompose()[1].z;
            var center:Vector3D = matrix.decompose()[0];
            centerX = center.x;
            centerY = center.y;

            if (i >= correlates.length)
            {
                correlate = itemPhysicsCorrelate.newInstance();
                correlate.initialize(
                    world,
                    -i,
                    centerX / pixelsToMeters,
                    centerY / pixelsToMeters,
                    angle,
                    widthPre / pixelsToMeters,
                    heightPre / pixelsToMeters
                    );
                correlates.push(correlate);
            }
            else
            {
                correlate = correlates[i];
                correlate.setSize(widthPre / pixelsToMeters, heightPre / pixelsToMeters);
                correlate.setPosition(
                    centerX / pixelsToMeters, 
                    centerY / pixelsToMeters, 
                    angle
                    );
            }
        }

        // DEBUG: don't try this at home.
        target.graphics.clear();
        target.drawRoundRect(0, 0, width, height, null, Math.random() *
            0xFFFFFF, 0.3);

        // TODO: "autoplay"?
        if (!isStepping)
            start();
    }

    /**
     *  @private
     */
    private function destroyCorrelates():void
    {
        if (!correlates)
            return;

        var correlate:IPhyicsCorrelate;
        for each (correlate in correlates)
            correlate.destroy();

        correlates = null;
    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private function validateWorldBoundaries(width:Number, height:Number):void
    {

        var halfWidth:Number = width / 2;
        var halfHeight:Number = height / 2;

        if (!boundingBox)
        {
            initializeBoundingBox(width, height);
            return;
        }

        var sizeChanged:Boolean;

        if (boundingBox.width != width)
        {

            boundingBox.bottomBody.DestroyFixture(boundingBox.bottomBody.GetFixtureList());
            boundingBox.topBody.DestroyFixture(boundingBox.topBody.GetFixtureList());

            b2Def.polygon.SetAsBox(halfWidth / pixelsToMeters, 0);
            b2Def.fixture.shape = b2Def.polygon;

            boundingBox.bottomBody.CreateFixture(b2Def.fixture);
            boundingBox.topBody.CreateFixture(b2Def.fixture);
            boundingBox.width = width;
            sizeChanged = true;
        }

        if (boundingBox.height != height)
        {
            boundingBox.leftBody.DestroyFixture(boundingBox.leftBody.GetFixtureList());
            boundingBox.rightBody.DestroyFixture(boundingBox.rightBody.GetFixtureList());

            b2Def.polygon.SetAsBox(0, halfHeight / pixelsToMeters);
            b2Def.fixture.shape = b2Def.polygon;

            boundingBox.leftBody.CreateFixture(b2Def.fixture);
            boundingBox.rightBody.CreateFixture(b2Def.fixture);

            boundingBox.height = height;
            sizeChanged = true;
        }

        if (!sizeChanged)
            return;

        boundingBox.rightBody.SetTransform(
            new V2(width / pixelsToMeters, halfHeight / pixelsToMeters), 0);

        boundingBox.leftBody.SetTransform(
            new V2(0, halfHeight / pixelsToMeters), 0);

        boundingBox.topBody.SetTransform(
            new V2(halfWidth / pixelsToMeters, 0), 0);

        boundingBox.bottomBody.SetTransform(
            new V2(halfWidth / pixelsToMeters, height / pixelsToMeters), 0);
    }

    /**
     *  @private
     */
    private function initializeDefaultWorld():void
    {
        world = new b2World(new V2(0, 9.8), true);
    }

    /**
     *  @private
     */
    private function initializeBoundingBox(width:Number, height:Number):void
    {
        var halfWidth:Number = width / 2;
        var halfHeight:Number = height / 2;
        boundingBox = new BoundingBox();

        b2Def.body.type = b2Body.b2_staticBody;
        b2Def.body.position.x = halfWidth / pixelsToMeters;
        b2Def.body.position.y = height / pixelsToMeters;
        b2Def.body.angle = 0;
        b2Def.polygon.SetAsBox(halfWidth / pixelsToMeters, 0);

        b2Def.fixture.shape = b2Def.polygon;
        b2Def.fixture.density = 0;

        boundingBox.bottomBody = world.CreateBody(b2Def.body);
        boundingBox.bottomBody.CreateFixture(b2Def.fixture);

        b2Def.body.position.y = 0;
        boundingBox.topBody = world.CreateBody(b2Def.body);
        boundingBox.topBody.CreateFixture(b2Def.fixture);

        b2Def.body.position.y = halfHeight / pixelsToMeters;
        b2Def.body.position.x = 0;
        b2Def.polygon.SetAsBox(0, halfHeight / pixelsToMeters);

        boundingBox.leftBody = world.CreateBody(b2Def.body);
        boundingBox.leftBody.CreateFixture(b2Def.fixture);

        b2Def.body.position.y = halfHeight / pixelsToMeters;
        b2Def.body.position.x = width / pixelsToMeters;
        b2Def.polygon.SetAsBox(0, halfHeight / pixelsToMeters);

        boundingBox.rightBody = world.CreateBody(b2Def.body);
        boundingBox.rightBody.CreateFixture(b2Def.fixture);

        boundingBox.width = width;
        boundingBox.height = height;
    }

    /**
     *  Start stepping through the physics simulation.
     */
    public function start():void
    {
        if (isStepping)
            return;

        setIsStepping(true);

        step();
        if (!timer)
            initializeTimer();

        timer.start();
    }

    /**
     *   Stop stepping through the physics simulation.
     */
    public function stop():void
    {
        if (!isStepping)
            return;

        setIsStepping(false);
        timer.stop();
    }

    /**
     *  @private
     */
    private function step():void
    {
        world.Step(debugStepFraction / stepFrequency, 10, 10);
        updatePositions();
    }

    /**
     *  @private
     */
    private function initializeTimer():void
    {
        timer = new Timer(1000 / stepFrequency, 0);
        timer.addEventListener(TimerEvent.TIMER, timer_timerHandler);
    }

    /**
     *  @private
     */
    private function updatePositions():void
    {
        var n:int = target.numElements;
        var element:ILayoutElement;
        var correlate:IPhyicsCorrelate;
        var matrix:Matrix3D;
        var width:Number;
        var height:Number;
        var x:Number;
        var y:Number;
        for (var i:int = 0; i < n; i++)
        {
            // FIX: use a proper map.
            element = useVirtualLayout ? 
                target.getVirtualElementAt(i) :
                target.getElementAt(i);

            width = element.getPreferredBoundsWidth(false);
            height = element.getPreferredBoundsHeight(false);

            correlate = correlates[i];
            x = correlate.centerX;
            y = correlate.centerY;

            matrix = new Matrix3D();
            matrix.appendTranslation(-width / 2, -height / 2, 0)

trace("correlate.angle", correlate.angle);

            matrix.appendRotation(
                correlate.angle * 180 / Math.PI, 
                Vector3D.Z_AXIS
                );
            matrix.appendTranslation(
                (x * pixelsToMeters), 
                (y * pixelsToMeters), 
                0
                );
            element.setLayoutMatrix3D(matrix, false);
        }
    }

    //--------------------------------------------------------------------------
    //
    //  Methods
    //
    //--------------------------------------------------------------------------

    /**
     *  @private
     */
    private function timer_timerHandler(event:TimerEvent):void
    {
        step();
    }
}
}

import Box2DAS.Dynamics.b2Body;

/**
 *  Helper class that bundles togehter the four bodies that make up the
 *  bouding box.
 */
class BoundingBox
{

    //--------------------------------------------------------------------------
    //
    //  Constructor
    //
    //--------------------------------------------------------------------------

    /**
     *  Constructor
     */
    public function BoundingBox():void
    {
        super();
    }

    //--------------------------------------------------------------------------
    //
    //  Properties
    //
    //--------------------------------------------------------------------------

    //--------------------------------------
    //  width
    //--------------------------------------

    /**
     *  Bounding box height.
     */
    public var width:Number;

    //--------------------------------------
    //  height
    //--------------------------------------

    /**
     *  Bounding box height.
     */
    public var height:Number;

    //--------------------------------------
    //  topBody
    //--------------------------------------

    /**
     *  Top bound body.
     */
    public var topBody:b2Body;

    //--------------------------------------
    //  bottomBody
    //--------------------------------------

    /**
     *  Bottom bound body.
     */
    public var bottomBody:b2Body;

    //--------------------------------------
    //  leftBody
    //--------------------------------------

    /**
     *  Left bound body.
     */
    public var leftBody:b2Body;

    //--------------------------------------
    //  rightBody
    //--------------------------------------

    /**
     *  Right bound body.
     */
    public var rightBody:b2Body;
}
